package caclient

import (
	"bytes"
	"encoding/json"
	"fmt"
	"gitee.com/wayjin/fabric-extension/cryptoutil"
	cfsslapi "github.com/cloudflare/cfssl/api"
	"github.com/mitchellh/mapstructure"
	"github.com/pkg/errors"
	"io/ioutil"
	"net/http"
	"net/url"
)

type Client struct {
	url       string
	caname    string
	admin     string
	adminpwd  string
	httpCli   *http.Client
	rootCAKey cryptoutil.KeyStore
}

var (
	ErrIdentityExist = errors.New("identity exist")
)

type keystore struct {
	CACert []byte
	SignCert []byte
	Key cryptoutil.Key
	Version string
	IssuerPublicKey []byte
	IssuerRevocationPublicKey []byte
}

func (ks keystore) GetCACert() []byte {
	return ks.CACert
}

func (ks keystore) GetSignCert() []byte {
	return ks.SignCert
}

func (ks keystore) GetPrivateKey() cryptoutil.Key {
	return ks.Key
}

func (ks keystore) GetVersion() string {
	return ks.Version
}

// new client with raw ca url like: http://admin:adminpw@localhost:7054
func NewClient(rawURL string) (*Client, error) {
	cli := &Client{
		httpCli:   &http.Client{},
	}
	purl, err := url.Parse(rawURL)
	if err != nil {
		return nil, fmt.Errorf("invalid url: %v", err)
	}
	url := fmt.Sprintf("%s://%s", purl.Scheme, purl.Host)

	cli.url = url
	username := purl.User.Username()
	password, _ := purl.User.Password()
	if username == "" {
		return cli, nil
	}
	rootCAKey, err := cli.enroll(EnrollmentRequest{Name: username, Secret:password})
	if err != nil {
		return nil, fmt.Errorf("connect CA server fail: %v", err)
	}

	cli.rootCAKey = rootCAKey
	return cli, nil
}

func (c *Client) getURL(uri string) string {
	return fmt.Sprintf("%s/%s", c.url, uri)
}

func (c *Client) send(req *http.Request, result interface{}) error {
	if c.httpCli == nil {
		c.httpCli = &http.Client{}
	}
	resp, err := c.httpCli.Do(req)
	if err != nil {
		return fmt.Errorf("client request fail: %v", err)
	}

	defer resp.Body.Close()
	bodyData, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	var res *cfsslapi.Response
	if bodyData != nil && len(bodyData) > 0 {
		res = new(cfsslapi.Response)
		err = json.Unmarshal(bodyData, res)
		if err != nil {
			return err
		}
		if len(res.Errors) > 0 {
			var errorMsg string
			for _, err1 := range res.Errors {
				msg := fmt.Sprintf("Response from server: Error Code: %d - %s\n", err1.Code, err1.Message)
				if errorMsg == "" {
					errorMsg = msg
				} else {
					errorMsg = errorMsg + fmt.Sprintf("\n%s", msg)
				}
			}
			return errors.Errorf(errorMsg)
		}
	}
	if resp.StatusCode >= 400 {
		return fmt.Errorf("invalid request code: %d", resp.StatusCode)
	}

	if res == nil {
		return fmt.Errorf("nil response result")
	}

	if result != nil {
		return mapstructure.Decode(res.Result, result)
	}
	return nil
}

func (c *Client) enroll(req EnrollmentRequest) (cryptoutil.KeyStore, error) {
	if req.CSR == nil {
		req.CSR = &cryptoutil.CSRInfo{
			CN:    req.Name,
			Hosts: []string{req.Name},
		}
	}

	certPEM, key, err := cryptoutil.GenerateKey(req.CSR, req.Name)
	if err != nil {
		return nil, err
	}

	reqNet := EnrollmentRequestNet{CAName: req.CAName, AttrReqs:req.AttrReq}
	reqNet.SignRequest.Request = string(certPEM)
	if req.Profile != "" {
		reqNet.Profile = req.Profile
	}
	reqNet.Label = req.Label
	body, err := cryptoutil.Marshal(&reqNet, "SignRequest")
	if err != nil {
		return nil, err
	}
	request, err := http.NewRequest("POST", c.getURL("enroll"), bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	request.SetBasicAuth(req.Name, req.Secret)
	var respNet EnrollmentResponseNet
	err = c.send(request, &respNet)
	if err != nil {
		return nil, err
	}

	cacert, _ := cryptoutil.B64Decode(respNet.ServerInfo.CAChain)
	signcert, _ := cryptoutil.B64Decode(respNet.Cert)
	issuePublic, _ := cryptoutil.B64Decode(respNet.ServerInfo.IssuerPublicKey)
	issueProcationPublic, _ := cryptoutil.B64Decode(respNet.ServerInfo.IssuerRevocationPublicKey)
	ks := keystore{
		CACert:  cacert,
		SignCert: signcert,
		Key: key,
		Version: respNet.ServerInfo.Version,
		IssuerPublicKey: issuePublic,
		IssuerRevocationPublicKey: issueProcationPublic,
	}
	return &ks, nil
}

func (c *Client) register(req RegistrationRequest, auth cryptoutil.KeyStore) error {
	if auth == nil {
		return fmt.Errorf("author key is nil")
	}
	id := &identity{
		cert:   auth.GetSignCert(),
		key:    auth.GetPrivateKey(),
		client: c,
	}
	_, err := id.getIdentity(req.Name, req.CAName)
	if err == nil {
		return ErrIdentityExist
	}
	if req.Affiliation != "" {
		_, err = id.getAffiliation(req.Affiliation, req.CAName)
		if err != nil {
			err = id.addAffiliation(req.Affiliation, req.CAName, true)
			if err != nil {
				return fmt.Errorf("add affiliation %s fail: %v", req.Affiliation, err)
			}
		}
	}

	fmt.Println("req => ", req)
	_, err = id.addIdentity(req)
	return err
}

func (c *Client) update(req ModifyIdentityRequest, auth cryptoutil.KeyStore) error {
	if auth == nil {
		return fmt.Errorf("author key is nil")
	}
	id := &identity{
		cert:   auth.GetSignCert(),
		key:    auth.GetPrivateKey(),
		client: c,
	}
	_, err := id.getIdentity(req.ID, req.Secret)
	if err != nil {
		return err
	}
	return  id.updateIdentity(req)
}

func (c *Client) Enroll(req EnrollmentRequest) (cryptoutil.KeyStore, error) {
	return c.enroll(req)
}

// Register: register role
func (c *Client) Register(req RegistrationRequest, auth cryptoutil.KeyStore) error {
	return c.register(req, auth)
}

// Update registered identity
func (c *Client) Update(req ModifyIdentityRequest, auth cryptoutil.KeyStore) error {
	return c.update(req, auth)
}

// Cancel: Cancellation of registration
func (c *Client) Cancel(name, caname string, auth cryptoutil.KeyStore) error {
	id := &identity{
		cert:   auth.GetSignCert(),
		key:    auth.GetPrivateKey(),
		client: c,
	}
	_, err := id.getIdentity(name, caname)
	if err != nil {
		return err
	}
	return id.delIdentity(name, caname, true)
}

func (c *Client) RegisterAndEnroll(req RegistrationRequest, auth cryptoutil.KeyStore) (cryptoutil.KeyStore, error) {
	var err error
	if auth == nil {
		fmt.Println("use root ca key")
		err = c.register(req, c.rootCAKey)
	} else {
		err = c.register(req, auth)
	}
	if err != nil && err != ErrIdentityExist {
		return nil, err
	}
	return c.enroll(EnrollmentRequest{Name: req.Name, Secret:req.Secret, Profile:req.Profile})
}

func (c *Client) GetIdentityCryptoCerts(req RegistrationRequest, auth cryptoutil.KeyStore, nodeOUs bool) (*IdentityCryptoCert, error) {
	// TODO
	mspCert, err := c.RegisterAndEnroll(req, auth)
	if err != nil {
		return nil, err
	}
	tlsCert, err := c.enroll(EnrollmentRequest{Name: req.Name, Secret:req.Secret, Profile:"tls"})
	if err != nil {
		return nil, err
	}
	idcert := &IdentityCryptoCert{admin: auth, mspcert:mspCert, tlscert:tlsCert, typ: req.Type, nodeOUs:nodeOUs}
	return idcert, nil
}
